iT邦幫忙

2026 iThome 鐵人賽

DAY 25
0
Software Development

從零打造邊緣運算閘道器:Raspberry Pi 與 Linux 底層軟硬整合實戰系列 第 25

Day 25:從隱性狀態到嚴謹架構:淺談有限狀態機 (FSM) 如何徹底解決爭奪鬧劇

  • 分享至 

  • xImage
  •  

🎯 問題情境 (The Problem)

在邊緣運算閘道器 (EdgeNode) 的開發中,我們設計了許多指示燈與系統狀態來反映機台當下的狀況。例如:

  • 插上設備時,開始同步資料 (藍燈閃爍)。
  • 同步成功後,提示可以拔除 (綠燈常亮)。
  • 如果設備發生錯誤或找不到資料 (橘燈閃爍)。
  • 系統發生致命錯誤 (紅燈閃爍)。

初期開發時,我們很直覺地在各個模組裡直接呼叫 led.turn_on_green() 或是 led.blink_blue()
然而,隨著系統越來越複雜,多個執行緒 (Thread) 開始並行運作:一個負責監聽 USB 拔插、一個負責與邊緣感測器 (MCU Node) 通訊、一個負責上傳雲端。
很快地,我們遇到了一個靈異現象:「狀態爭奪鬧劇」。

舉例來說,當 MCU Node 下載成功,準備亮起「綠燈」的瞬間,如果剛好遇到網路斷線,上傳模組會立刻觸發「橘燈」。最後燈號就會卡在半綠半橘,或者發生系統以為在待機,但燈號卻閃著錯誤的詭異狀況。

❌ 錯誤嘗試 (The Pitfalls)

為了解決這個問題,工程師最常做的直覺反應就是:加 Flag (旗標)
於是,我們的程式碼裡面開始充滿了各種隱性狀態 (Implicit State):

if not is_error and is_sync_done and not is_uploading:
    led.turn_on_green()
elif is_uploading and not is_sync_done:
    led.blink_blue()
# ... 無止盡的 if-else 地獄

這種做法有幾個致命缺點:

  1. 狀態爆炸:當你有 3 個 Boolean 變數,你就有 2³ = 8 種潛在狀態。其中很多狀態是互相矛盾的(例如 is_sync_done = True 但同時 is_uploading = True?)。
  2. 競態條件 (Race Condition):當 Thread A 剛檢查完 is_error == False 準備亮燈時,Thread B 瞬間把 is_error 改成了 True。這時候你的系統狀態就與硬體表現脫節了。
  3. 難以除錯:每次出問題,你只能去追蹤那十幾個 Flag 到底在哪個時間點被改變了。

🧠 底層原理:隱性狀態 vs. 顯性狀態

為什麼用 if-else 和 Flag 會這麼痛苦?因為這些變數只是「隱性地」拼湊出一個狀態。
在軟體工程中,如果你的系統有特定的「生命週期」或「階段」,你應該使用 有限狀態機 (Finite State Machine, FSM) 來將狀態「顯性化」。

有限狀態機有幾個核心概念:

  1. 狀態 (State):系統在任何時間點,都必須處於「唯一」的一個確切狀態(例如:IDLE, SYNCING, ERROR, SUCCESS)。
  2. 事件 (Event) / 轉移 (Transition):狀態只能透過明確的事件來切換,而且只能切換到允許的下一個狀態。
  3. 進入/退出動作 (Entry/Exit Action):進入某個狀態時,強制執行該狀態應有的行為(例如:進入 ERROR 狀態就強制亮橘燈)。

💡 最終解決方案:套用 FSM 嚴謹架構

為了解決這場爭奪鬧劇,我們決定拔除所有散落在各處的 if-else 與 Flag,為 EdgeNode 建立一個集中的狀態管理中心。

1. 定義明確的狀態列舉 (Enum)

我們先把所有可能的情境收斂成幾個絕對互斥的狀態:

from enum import Enum, auto

class SystemState(Enum):
    IDLE = auto()          # 待機 (等待設備插入)
    SYNCING = auto()       # 處理中 (下載/上傳資料)
    SUCCESS = auto()       # 成功 (可拔除)
    ERROR = auto()         # 發生錯誤

2. 實作集中式狀態機與轉移邏輯

接下來,我們實作一個 StateManager,所有的 Thread 都不能自己去控制硬體,只能對 StateManager 送出「事件 (Event)」。

import threading

class EdgeNodeFSM:
    def __init__(self):
        self.state = SystemState.IDLE
        self._lock = threading.Lock()
        
    def transition(self, new_state):
        with self._lock:
            # 狀態轉移的防呆邏輯
            if self.state == SystemState.ERROR and new_state != SystemState.IDLE:
                print("系統已處於錯誤狀態,必須先回到 IDLE!")
                return False
                
            print(f"狀態轉移:{self.state.name} -> {new_state.name}")
            self.state = new_state
            self._apply_hardware_state()
            return True
            
    def _apply_hardware_state(self):
        # 將硬體行為 (LED) 綁定到唯一狀態
        if self.state == SystemState.IDLE:
            led.turn_off()
        elif self.state == SystemState.SYNCING:
            led.blink_blue()
        elif self.state == SystemState.SUCCESS:
            led.turn_on_green()
        elif self.state == SystemState.ERROR:
            led.blink_orange()

3. 完美的結局

導入 FSM 後,不管背後有多少個 Thread 在平行運作,它們能做的事情只有呼叫 fsm.transition(SystemState.ERROR)

  • 因為有 threading.Lock(),我們徹底消滅了 Race Condition。
  • 因為有狀態綁定,進入什麼狀態就亮什麼燈,硬體表現絕對不會跟軟體邏輯脫節。
  • 當某個 Thread 在 SUCCESS 狀態下試圖觸發 SYNCING 時,FSM 可以直接拒絕這個不合法的轉移,保護了系統的完整性。

🎯 小結

從「隱性的 Flag 地獄」升級到「嚴謹的 FSM 狀態機」,是把邊緣運算閘道器從「能動就好 (Prototype)」推向「商用穩定 (Production)」的關鍵一步。它不僅讓程式碼更優雅,更從架構層面直接免疫了多執行緒的爭奪鬧劇。

在下一篇,我們將邁入最終章節:SRE 穩定度工程。來看看我們在佈署機器到偏遠場域前,還做了哪些「防禦性」的前置準備!


上一篇
[Day 24] 硬體通訊的眉角:time.sleep() 帶來的災難與 Debounce (防彈跳) 實戰
下一篇
Day 26:開源前的準備:實戰機敏資訊去敏與自動化發布檢查
系列文
從零打造邊緣運算閘道器:Raspberry Pi 與 Linux 底層軟硬整合實戰26
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言